探索 Module Federation 下的应用热更新方案

Tags
Web Dev
Service Worker
Published
June 3, 2022
Author
HJS
关于 MF 的细节不过多讲解,如果你不是很了解它,建议从官方文档开始。为了下文更好的表达,我们提前约定一些术语:
  • MF:Module Federation
  • Host:宿主应用
  • Remote:远程模块
 

MF 热更新

在传统的 MF 应用中,如果 Remote 模块已经加载过一次,除非应用重启,否则即使期间 Remote 发生更新也无法更新到 Host。而“热更新”指的是在无需重启应用的情况下,能够重新获取 Remote 模块。具体来说可以拆分为以下 3 点:
  1. 检测 remote module 存在更新
  1. 清理内存中旧的 module
  1. 重新加载 module 对应的组件
 

检测模块更新

前端实现检测文件更新最直接的方式是使用 Service Worker,而无需在服务端进行额外的动作。由于作者本人真实业务的特点,希望 Host 和 Remote 尽可能的解耦。所以架构上会由 Remote 接管子路由,并且在该 Scope 下注册自己的 Service Worker。例如:
if ("serviceWorker" in navigator) { const wb = new Workbox("/sub-app-path/service-worker.js"); wb.addEventListener("waiting", () => { wb.addEventListener("controlling", () => { // callback(); }); wb.messageSkipWaiting(); }); wb.register(); }
 

清理旧的 module 缓存

如果有看过 MF 构建产物会看到,由一个全局变量 chunkLoadingGlobal 维护 chunkId 和 module 的映射关系,首次使用时 JSONP 下载 module 并设置缓存。因此很容易在这个全局变量中移除掉 module 缓存。
  • 首先设置 webpack 配置的 “chunkLoadingGlobal” 和 “chunkIds” 为可读的属性
  • 然后通过使用以下方法删除具体的 scope 和 module
const clearModules = (scope: string, modules?: Array<string>) => { if (!window[scope]) return if (!modules) { window[scope] = [] } else { const set = new Set(modules) window[scope].forEach((chunk, index) => { const [name] = chunk[0] if (set.has(name)) { delete window[scope][index] } }) } }
 

重新加载 module 对应的组件

官方示例中已经提供了动态加载的模版代码,这里使用 HOC 使得动态加载的组件都会传入 forceReload 用来重新加载组件。
const loadComponentAdvance = (url, scope, module) => { const loader = () => loadComponent(url, scope, module) return (props) => { const [Comp, setComp] = useState(() => loader()) const forceReload = () => { setComp(() => loader()) } return <Comp {...props} forceReload={forceReload} /> } }
 
然后只需要将 forceReload 放到 Service Worker 检测到更新的 callback 位置即可实现热更新。
 
 

结语

由于并不是官方提供的用法,建议锁定依赖版本使用 🌝。